package manager

import (
	"context"
	"happy-sudoku/graphql/models"
	"happy-sudoku/internal/codec"
	logging "happy-sudoku/internal/infr"
	"happy-sudoku/internal/model"
	"happy-sudoku/internal/repository"

	"github.com/pkg/errors"
)

// IRoomManager to be implemented by RooManager
type IRoomManager interface {
	// Exist return whether the room existing
	Exist(ctx context.Context, roomID int) bool
	// Delete deletes room, update killoff all player
	Delete(ctx context.Context, roomID int) error
	HandleLeaveRoom(ctx context.Context, playerID int) error
	// Unsubscribe the RoomList update that player has subscribed
	UnsubscribeRoomList(ctx context.Context, playerID int)
}

const roomManagerErrHeader = "RoomManager."

// RoomManager manages Room
type RoomManager struct {
	roomRepo    repository.IRoomRepo
	puzzleMgr   IPuzzleManager
	playerMgr   IPlayerManager
	roomSyncMgr IRoomSyncManager
}

// RoomManagerCfg use to build RoomManager
type RoomManagerCfg struct {
	RoomRepo    repository.IRoomRepo
	PuzzleMgr   IPuzzleManager
	PlayerMgr   IPlayerManager
	RoomSyncMgr IRoomSyncManager
}

// NewRoomManager new RoomManager
func NewRoomManager(cfg RoomManagerCfg) *RoomManager {
	return &RoomManager{
		roomRepo:    cfg.RoomRepo,
		puzzleMgr:   cfg.PuzzleMgr,
		playerMgr:   cfg.PlayerMgr,
		roomSyncMgr: cfg.RoomSyncMgr,
	}
}

// Exist returns whether the room exists
func (r *RoomManager) Exist(ctx context.Context, id int) bool {
	return r.roomSyncMgr.Exist(ctx, id)
}

// Delete deletes room, update killoff all player
func (r *RoomManager) Delete(ctx context.Context, roomID int) error {
	const errHeader = roomManagerErrHeader + "Delete"

	// Ignore, when no room (log)
	if !r.Exist(ctx, roomID) {
		logging.ForLoggerContext(ctx).Warn(codec.ErrRoom404.WithField("roomID", roomID).Wrap(errHeader).Error())
		return nil
	}

	// DestroyAll pubsub in room
	r.roomSyncMgr.DestroyAll(ctx, roomID)

	// Notify change
	r.roomSyncMgr.PublishRoomListUpdated(ctx, models.RoomListUpdatedRes{
		Room: models.Node{
			ID: roomID,
		},
		Kind: models.RoomListUpdatedKindDeleted,
	})

	err := r.roomRepo.Delete(roomID)
	return errors.Wrap(err, errHeader)
}

// HandleLeaveRoom execute when player is about to leave room
func (r *RoomManager) HandleLeaveRoom(ctx context.Context, playerID int) error {
	const errHeader = roomManagerErrHeader + "HandleLeaveRoom"

	// get player state
	playerState, err := r.playerMgr.GetPlayerState(playerID)
	if err != nil {
		nerr := errors.Wrap(err, errHeader)
		// not return to client, but log
		if codec.ErrPlayerState404.IsErrorEqual(err) {
			logging.ForLoggerContext(ctx).Warn(nerr)
			return nil
		}
		return nerr
	}

	// get room id
	roomID := playerState.Location.RoomID
	// Player not in the room, ignore
	if roomID == 0 {
		return nil
	}
	if !r.Exist(ctx, roomID) {
		return errors.Wrap(codec.ErrRoom404.WithField("roomID", roomID), errHeader)
	}

	// Unsubscribe first to avoid error
	r.roomSyncMgr.UnsubscribeAll(ctx, roomID, playerID)
	room, err := r.roomRepo.GetOne(roomID)
	if err != nil {
		return errors.Wrap(err, errHeader)
	}
	// Update repo
	err = r.roomRepo.PopPlayer(room, playerID)
	if err != nil {
		return errors.Wrap(err, errHeader)
	}

	// Update player status
	newPlayerState := model.NewPlayerState(model.PlayerStateCfg{
		PlayerID:    playerID,
		Status:      model.StatusWandering,
		Progress:    0,
		GamingScore: 0,
		Location: model.PlayerLocation{
			RoomID: 0,
			Seat:   0,
		},
	})
	_, err = r.playerMgr.UpdatePlayerState(playerID, *newPlayerState)

	if err != nil {
		// Warning if update playerState failed
		logger := logging.ForLoggerContext(ctx)
		logger.Warn(err)
	}

	// if no one is in the room, delete the room
	if len(room.PlayersID) == 0 {
		err := r.Delete(ctx, roomID)
		return errors.Wrap(err, errHeader)
	}

	//TODO: if the host leaves the room, reassign one
	if playerID == room.HostID {
		// Select one as host
		hostID := room.PlayersID[0]
		// update Host
		err := r.roomRepo.UpdateHostID(room, hostID)
		if err != nil {
			return errors.Wrap(err, errHeader)
		}
	}

	// Publish State
	go func() {
		r.roomSyncMgr.PublishRoomToPlayers(ctx, roomID, *room)
	}()

	return nil
}

// UnsubscribeRoomList the RoomList update that player has subscribed
func (r *RoomManager) UnsubscribeRoomList(ctx context.Context, playerID int) {
	r.roomSyncMgr.UnsubscribeRoomListUpdated(ctx, playerID)
}
